#!/bin/sh /etc/rc.common
# Copyright (C) 2006-2008 OpenWrt.org
# Copyright (C) 2019 Jeffery To

START=90
USE_PROCD=1

PID_FILE="/var/run/stunnel/stunnel.pid"
CONF_FILE="/var/etc/stunnel.conf"
BIN="/usr/bin/stunnel"
CONF_FILE_CREATED=
HAVE_ALT_CONF_FILE=
SERVICE_SECTION_FOUND=

validate_globals_section() {
	uci_load_validate stunnel globals "$1" "$2" \
		'alt_config_file:file' \
		\
		'compression:or("deflate","zlib")' \
		'EGD:string' \
		'engine:string' \
		'engineCtrl:string' \
		'engineDefault:list(or("ALL","CIPHERS","DH","DIGESTS","DSA","ECDH","ECDSA","PKEY","PKEY_ASN1","PKEY_CRYPTO","RAND","RSA"))' \
		'log:or("append","overwrite")' \
		'output:string' \
		'RNDbytes:uinteger' \
		'RNDfile:string' \
		'RNDoverwrite:bool' \
		'setgid:or(string,uinteger)' \
		'setuid:or(string,uinteger)' \
		'syslog:bool' \
		;
}

validate_service_section() {
	uci_load_validate stunnel service "$1" "$2" \
		'enabled:bool:1' \
		\
		'setgid:or(string,uinteger)' \
		'setuid:or(string,uinteger)' \
		;
}

validate_service_options() {
	uci_load_validate stunnel "$1" "$2" "$3" \
		'accept_host:host' \
		'accept_port:port' \
		'CAfile:string' \
		'CApath:string' \
		'cert:string' \
		'checkEmail:list(string)' \
		'checkHost:list(host)' \
		'checkIP:list(ipaddr)' \
		'ciphers:list(string)' \
		'ciphersuites:list(string)' \
		'client:bool' \
		'config:list(string)' \
		'connect:list(string)' \
		'CRLfile:string' \
		'CRLpath:string' \
		'curves:list(string)' \
		'debug:or(range(0,7),string)' \
		'delay:bool' \
		'engineId:string' \
		'engineNum:and(uinteger,min(1))' \
		'exec:string' \
		'execArgs:string' \
		'failover:or("prio","rr")' \
		'ident:string' \
		'include:directory' \
		'key:string' \
		'local:host' \
		'logId:or("process","sequential","thread","unique")' \
		'OCSP:string' \
		'OCSPaia:bool' \
		'OCSPflag:list(or("NOCASIGN","NOCERTS","NOCHAIN","NOCHECKS","NODELEGATED","NOEXPLICIT","NOINTERN","NOSIGS","NOTIME","NOVERIFY","RESPID_KEY","TRUSTOTHER"))' \
		'OCSPnonce:bool' \
		'options:list(string)	' \
		'protocol:or("cifs","connect","imap","nntp","pgsql","pop3","proxy","smtp","socks")' \
		'protocolAuthentication:or("basic","login","ntlm","plain")' \
		'protocolDomain:hostname' \
		'protocolHost_host:host' \
		'protocolHost_port:port' \
		'protocolPassword:string' \
		'protocolUsername:string' \
		'PSKidentity:string' \
		'PSKsecrets:string' \
		'pty:bool' \
		'redirect_host:host' \
		'redirect_port:port' \
		'renegotiation:bool' \
		'requireCert:bool' \
		'reset:bool' \
		'retry:bool' \
		'service:string' \
		'sessionCacheSize:uinteger' \
		'sessionCacheTimeout:uinteger' \
		'sessiond_host:host' \
		'sessiond_port:port' \
		'sni:list(string)' \
		'socket:list(string)' \
		'sslVersion:or("all","SSLv2","SSLv3","TLSv1","TLSv1.1","TLSv1.2")' \
		'stack:uinteger' \
		'ticketKeySecret:string' \
		'ticketMacSecret:string' \
		'TIMEOUTbusy:uinteger' \
		'TIMEOUTclose:uinteger' \
		'TIMEOUTconnect:uinteger' \
		'TIMEOUTidle:uinteger' \
		'transparent:or("both","destination","none","source")' \
		'verifyChain:bool' \
		'verifyPeer:bool' \
		;
}

validate_globals_section_service_options() {
	validate_service_options globals "$@"
}

validate_service_section_service_options() {
	validate_service_options service "$@"
}

print_options() {
	local _opt
	local _value
	for _opt in "$@"; do
		eval "_value=\$$_opt"
		[ -z "$_value" ] || echo "$_opt = $_value" >> "$CONF_FILE"
	done
}

print_bool_options() {
	local _opt
	local _bool
	local _value
	for _opt in "$@"; do
		eval "_bool=\$$_opt"
		[ -z "$_bool" ] || {
			_value=no
			[ "$_bool" != 1 ] || _value=yes
			echo "$_opt = $_value" >> "$CONF_FILE"
		}
	done
}

print_lists_map() {
	local _opt
	local _values
	local _value
	for _opt in "$@"; do
		eval "_values=\$$_opt"
		for _value in $_values; do
			echo "$_opt = $_value" >> "$CONF_FILE"
		done
	done
}

print_lists_reduce() {
	local _delim="$1"
	local _opt
	local _value
	local _values
	local _v
	shift
	for _opt in "$@"; do
		_value=
		eval "_values=\$$_opt"
		for _v in $_values; do
			_value=$_value$_delim$_v
		done
		_value=${_value#$_delim}
		[ -z "$_value" ] || echo "$_opt = $_value" >> "$CONF_FILE"
	done
}

print_host_port() {
	local _opt
	local _host
	local _port
	for _opt in "$@"; do
		eval "_host=\${${_opt}_host}"
		eval "_port=\${${_opt}_port}"
		[ -z "$_host" ] || [ -z "$_port" ] || echo "$_opt = $_host:$_port" >> "$CONF_FILE"
	done
}

print_optional_host_port() {
	local _opt
	local _host
	local _port
	local _value
	for _opt in "$@"; do
		eval "_host=\${${_opt}_host}"
		eval "_port=\${${_opt}_port}"
		[ -z "$_port" ] || {
			_value=$_port
			[ -z "$_host" ] || _value=$_host:$_port
			echo "$_opt = $_value" >> "$CONF_FILE"
		}
	done
}

print_global_options() {
	print_options \
		compression \
		EGD \
		engine \
		engineCtrl \
		log \
		output \
		RNDbytes \
		RNDfile \
		RNDoverwrite \
		;

	print_bool_options \
		syslog \
		;

	print_lists_reduce , \
		engineDefault \
		;
}

print_service_options() {
	[ "$2" = 0 ] || {
		echo "validation failed"
		return 1
	}

	print_options \
		CAfile \
		CApath \
		cert \
		CRLfile \
		CRLpath \
		debug \
		logId \
		engineId \
		engineNum \
		exec \
		execArgs \
		failover \
		ident \
		include \
		key \
		local \
		OCSP \
		protocol \
		protocolAuthentication \
		protocolDomain \
		protocolPassword \
		protocolUsername \
		PSKidentity \
		PSKsecrets \
		service \
		sessionCacheSize \
		sessionCacheTimeout \
		setgid \
		setuid \
		sslVersion \
		stack \
		ticketKeySecret \
		ticketMacSecret \
		TIMEOUTbusy \
		TIMEOUTclose \
		TIMEOUTconnect \
		TIMEOUTidle \
		transparent \
		;

	print_bool_options \
		client \
		delay \
		OCSPaia \
		OCSPnonce \
		pty \
		renegotiation \
		requireCert \
		reset \
		retry \
		verifyChain \
		verifyPeer \
		;

	print_lists_map \
		checkEmail \
		checkHost \
		checkIP \
		config \
		connect \
		OCSPflag \
		options \
		sni \
		socket \
		;

	print_lists_reduce : \
		ciphers \
		curves \
		ciphersuites \
		;

	print_host_port \
		protocolHost \
		sessiond \
		;

	print_optional_host_port \
		accept \
		redirect \
		;
}

create_conf_file() {
	[ -n "$CONF_FILE_CREATED" ] || {
		mkdir -p "$(dirname "$CONF_FILE")"
		echo "; STunnel configuration file generated by uci" > "$CONF_FILE"
		echo "; Written $(date +'%c')" >> "$CONF_FILE"
		echo >> "$CONF_FILE"
		echo "foreground = quiet" >> "$CONF_FILE"
		echo "pid = $PID_FILE" >> "$CONF_FILE"
		CONF_FILE_CREATED=1
	}
}

global_defs() {
	local pid_dir

	[ "$2" = 0 ] || {
		echo "validation failed"
		return 1
	}

	# If the first globals section has alt_config_file, don't process any more globals
	[ -z "$HAVE_ALT_CONF_FILE" ] || return 0

	# If "alt_config_file" specified in the first globals section, use that instead
	[ -z "$alt_config_file" ] || [ -n "$CONF_FILE_CREATED" ] || {
		# Symlink "alt_config_file" since it's a bit easier and safer
		ln -s "$alt_config_file" "$CONF_FILE"
		# Set section found to start service, user hopefully knows what they are doing
		SERVICE_SECTION_FOUND=1
		CONF_FILE_CREATED=1
		HAVE_ALT_CONF_FILE=1
		return 0
	}

	pid_dir="$(dirname "$PID_FILE")"
	mkdir -p "$pid_dir"
	[ -z "$setuid" ] || chown "$setuid" "$pid_dir"
	[ -z "$setgid" ] || chown ":$setgid" "$pid_dir"

	create_conf_file
	print_global_options
	validate_service_options globals "$1" print_service_options
}

service_section() {
	[ "$2" = 0 ] || {
		echo "validation failed"
		return 1
	}

	[ "$enabled" = 1 ] || return 0

	SERVICE_SECTION_FOUND=1
	echo >> "$CONF_FILE"
	echo "[$1]" >> "$CONF_FILE"

	validate_service_options service "$1" print_service_options
}

service_triggers() {
	procd_add_reload_trigger stunnel

	procd_open_validate
	validate_globals_section "$@"
	validate_globals_section_service_options "$@"
	validate_service_section "$@"
	validate_service_section_service_options "$@"
	procd_close_validate
}

start_service() {
	rm -f "$CONF_FILE"
	config_load stunnel

	config_foreach validate_globals_section globals global_defs

	[ -n "$HAVE_ALT_CONF_FILE" ] || {
		create_conf_file
		config_foreach validate_service_section service service_section
	}

	[ -n "$SERVICE_SECTION_FOUND" ] || {
		logger -t stunnel -p daemon.info "No uci service section enabled or found!"
		return 1
	}

	procd_open_instance
	procd_set_param command "$BIN"
	procd_append_param command "$CONF_FILE"
	procd_set_param respawn
	procd_set_param file "$CONF_FILE"
	procd_close_instance
}
